import zmq
import uuid
import threading
import pybonjour
import select
import socket
import xml
import xml.etree.ElementTree as ET
import math
import time
import logging
dnsLogger = logging.getLogger("DNS")
msgLogger = logging.getLogger("MSG")
msgSendLogger = logging.getLogger("MSG.SEND")
msgRecvLogger = logging.getLogger("MSG.RECV")
peerLogger = logging.getLogger("PEER")
nsoLogger = logging.getLogger("NSO")
nsoXMLLogger = logging.getLogger("NSO.XML")
xmlLogger = logging.getLogger("XML")
xmlSetLogger = logging.getLogger("XML")
xmlAddLogger = logging.getLogger("XML.ADD")
xmlDelLogger = logging.getLogger("XML.DEL")
xmlReplLogger = logging.getLogger("XML.REPL")
xmlGSipLogger = logging.getLogger("XML.GSIP")

# local modules
from .DerbyXML import DerbyXML
from .NSObject import NSObject

from .Timestamp import Timestamp
from .Timestamp import SecondsToHMS

import BoutStates
import Defaults

PEER = 'PEER' # body contains address:port of new peer
HELO = 'HELO' # body contains address:port of sender
NAME = 'NAME' # body contains current user visible name
CHAT = 'CHAT' # body contains arbitrary text
XMSG = 'XMSG' # body contains XML
GBYE = 'GBYE' # body is empty/ignored
GSIP = 'GSIP' # body contains name of peer, sequence number

mDNSTYPE = "_nsode_derbyos_com._tcp"
def ConvertToIPAddress(address):
        resolved = socket.getaddrinfo(address,80)
        for info in resolved:
            if info[0] == socket.AF_INET:
                return info[4][0] # get the raw ip number instead
        return address

class NSOdePeer(NSObject):
    """ NSOdePeer represents a peer (not the current process) """
    def __init__(self, ctx, address, port, nsode, uuid = None):
        NSObject.__init__(self)
        self.uuid = uuid
        # convert to raw-ip address
        self.address = ConvertToIPAddress(address)
        self.port = port
        self.ctx = ctx
        self.mailbox = None
        self.connected = False
        self.nsode = nsode
        self.serviceName = '???'
        self.ready = False
    
    def __del__(self):
        self.disconnect()
        
    def connect(self):
        """ Connect to remote node's inbox, if not already connected """
        if self.connected:
            return
        # Create new outgoing socket (drop any messages in transit)
        self.mailbox = zmq.Socket(self.ctx, zmq.DEALER)
        # Set our caller 'From' identity so that receiving node knows
        # who each message came from.
        self.mailbox.setsockopt(zmq.IDENTITY, self.nsode.uuid.bytes)
        # Set a high-water mark that allows for reasonable activity
        self.mailbox.setsockopt(zmq.SNDHWM, 1000)
        # Send messages immediately or return EAGAIN
        self.mailbox.setsockopt(zmq.SNDTIMEO, 0)
        # Connect through to peer node
        #print("tcp://%s" %endpoint)
        self.mailbox.connect("tcp://%s:%d" %(self.address,self.port))
        self.connected = True
        self.ready = False
        if not self.uuid:
            self.uuid = '???'
        # send the "i am" message
        self.tellPeer(HELO,"%s:%d" % (self.nsode.address, self.nsode.port))
    
    def disconnect(self):
        if not self.connected:
            return
        self.mailbox.close()
        self.mailbox = None
        self.connected = False
        

    def __str__(self):
        return "<NSOdePeer " + str(self.uuid)+'/'+self.serviceName + " %s:%d>" % (self.address,self.port)

    def tellPeer(self, msg, body):
        if not self.connected:
            return
        """ Send a message (as a UTF8 unicode string) to a given peers """
        # already sent
        #self.mailbox.send_multipart(self.nsode.uuid.bytes, flags=zmq.SNDMORE)
        self.mailbox.send_unicode(msg, flags=zmq.SNDMORE)
        self.mailbox.send_unicode(body)
        peerLogger.debug("%s: Telling peer %s %s",self,msg,body)


class NSOdeResolver(object):
    """ Used to resolve (via mdns) and broadcast our node """
    def __init__(self, regtype = mDNSTYPE):
        self.regtype = mDNSTYPE
        self.sdRefRegister = None
        self.sdRefBrowse = None
        self.sdRefResolve = []
        self.thread = None
        self.registered = False
        self.polling = False
    
    def beginPollingInThread(self):
        if self.thread:
            return
        self.polling = True
        def background():
            peerLogger.debug("Looking for peers")
            while self.polling:
                try:
                    try:
                        while True:
                            ports = []
                            if self.sdRefRegister is not None:
                                ports.append(self.sdRefRegister)
                            if self.sdRefBrowse is not None:
                                ports.append(self.sdRefBrowse)
                            if self.sdRefResolve:
                                ports = ports + self.sdRefResolve
                            ready = select.select(ports, [], [])
                            for port in ready[0]:
                                if port == self.sdRefRegister:
                                    peerLogger.debug("Registered our entry")
                                elif port == self.sdRefBrowse:
                                    peerLogger.debug("Found entry")
                                elif port in self.sdRefResolve:
                                    peerLogger.debug("Resolved entry")
                                pybonjour.DNSServiceProcessResult(port)
                    except KeyboardInterrupt:
                        pass
                finally:
                    if self.sdRefRegister is not None:
                        self.sdRefRegister.close()
                    if self.sdRefBrowse is not None:
                        self.sdRefBrowse.close()
                    for port in self.sdRefResolve:
                        port.sdRefResolve.close()
        self.thread = threading.Thread(target=background)
        #thread.daemon = True
        self.thread.start()
        
    def broadcastIdentity(self, aname, aport, uuid, boutid):
        """ Subclass provides interface to mDNS """
        def register_callback(sdRef, flags, errorCode, name, regtype, domain):
            if errorCode == pybonjour.kDNSServiceErr_NoError:
                peerLogger.info("Registered service:\n  name    =%s\n  regtype =%s\n  domain  =%s", name, regtype, domain)
                self.registered = True
        import select
        self.registered = False
        peerLogger.info("Registering %s %s %s",aname,self.regtype,aport)
        self.sdRefRegister = pybonjour.DNSServiceRegister(name = str(aname),
                                             regtype = self.regtype,
                                             port = int(aport),
                                             callBack = register_callback,
                                             txtRecord = pybonjour.TXTRecord({'uuid': uuid,'bout':boutid}))
        peerLogger.debug("Identity broadcast")
        self.beginPollingInThread()

    def lookForPeers(self, addPeer, removePeer):
        """ addPeer(fullname,hosttarget,port, record) 
        removePeer(serviceName)
        """
        peerLogger.info("Starting to look for peers")
        # the callback when an entry is actually resolved
        def resolve_callback(sdRef, flags, interfaceIndex, errorCode, fullname,
                             hosttarget, port, txtRecord):
            if errorCode == pybonjour.kDNSServiceErr_NoError:
                record = pybonjour.TXTRecord.parse(txtRecord)
                peerLogger.info('Resolved service:\n  fullname   =%s\n  hosttarget =%s\n  port       =%s\n  txtRecord  =%s',fullname, hosttarget, port, record)
                addPeer(fullname, hosttarget, port, record)
            self.sdRefResolve.remove(sdRef) # and we no longer need to wait for this
        
        def browse_callback(sdRef, flags, interfaceIndex, errorCode, serviceName, regtype, replyDomain):
            if errorCode != pybonjour.kDNSServiceErr_NoError:
                return
        
            if not (flags & pybonjour.kDNSServiceFlagsAdd):
                peerLogger.info("Service %s removed",serviceName)
                removePeer(serviceName)
                return
        
            peerLogger.debug("Service added; resolving")
            # this request probably should time out?
            resolve_sdRef = pybonjour.DNSServiceResolve(0,
                                                        interfaceIndex,
                                                        serviceName,
                                                        regtype,
                                                        replyDomain,
                                                        resolve_callback)
            self.sdRefResolve.append(resolve_sdRef)
        self.sdRefBrowse = pybonjour.DNSServiceBrowse(regtype = mDNSTYPE,
                                                  callBack = browse_callback)
        self.beginPollingInThread()
                                        
class NSOGossip(object):
    """ NSOGossip manages the gossip based synchronization
    """
    def __init__(self):
        self.audit = []
        self.byPeers = {}
        self.lastGossip = {}
    def latestFromPeer(self, peer):
        """
        Figure out what was the last (contiguous) message sequence number that we got
         from a given peer
        @param peer: The peer to check (service name)
        @return: The last contiguous message, or zero
        """
        seen = self.byPeers.get(peer,{})
        for i in range(1,len(seen)+1):
            if not seen.has_key(i):
                return i-1 # the last one we saw was the last contiguous
        return len(seen) # all are good
    def gossip(self, peer, sequence):
        """
        Record a gossip message
        @param peer: Source (service name)
        @param sequence: sequence number
        @return: true if some action needs to be taken
        """
        if self.lastGossip.get(peer,0) == sequence:
            xmlGSipLogger.info("Gossip %s:%d up to date", peer, sequence)
            return False
        # this doesn't match our understanding
        xmlGSipLogger.warning("Gossip %s:%d - we are behind", peer, sequence)
        self.lastGossip[peer] = sequence
        return True
    def recordMessage(self, msg):
        """
        @param msg: The xmsg body xml (ET.Element - NOT DerbyXML)
        @return: false if the message has already been seen (and so can be ignored)
            None if the message hasn't been seen, but is not the next sequence message
            true if hasn't been seen, but is the next sequence message
        """
        source = msg.get('source')
        sequence = int(msg.get('sequence'))
        self.audit.append(msg)
        if self.byPeers.has_key(source):
            peerMsgs = self.byPeers[source]
            if not peerMsgs.get(sequence):
                peerMsgs[sequence] = msg
                if self.latestFromPeer(source) == sequence:
                    self.lastGossip[source] = sequence # same effect as a gossip message
                    return True # contiguous, so this was the next
                xmlGSipLogger.warning("Missing some gossip from %s before %d",source,sequence)
                return None # we know that there are gaps
            return False # already seen it
        else:
            self.byPeers[source] = {sequence: msg}
            if sequence == 1:
                self.lastGossip[source] = 1 # same effect as a gossip message
                return True # it is the next sequence number
            self.lastGossip[source] = sequence # this is out of sync
            xmlGSipLogger.warning("Initial gossip from %s out of sync: %d",source,sequence)
            return None
    def playback(self, source, sequence):
        """
        Look through the audit to find a messages from a given source and sequence
        @param source: The source
        @param sequence: The sequenece number
        @return: xmsg string if we have a record of it, None if not
        """
        return ET.tostring(self.byPeers.get(source,{}).get(sequence))
    def missingMessages(self):
        """
        Find what peers we are missing messages from
        @return:
        """
        retval = []
        for source in self.byPeers:
            if self.lastGossip[source] != self.latestFromPeer(source):
                retval.append(source) # we are missing something here
        return retval


class NSOde(NSObject):
    """ NSOde is a node representing the current process
    """
    def __init__(self, ctx=zmq.Context()):
        """

        @type ctx: zmq context
        """
        NSObject.__init__(self)
        import Defaults
        self.ctx = ctx
        self.gossip = NSOGossip()
        self.bout = Defaults.BoutName
        self.inbox = ctx.socket(zmq.ROUTER)
        self.port = self.inbox.bind_to_random_port("tcp://*")
        self.peers = []
        self.watchers = {}
        self.derbyXML = Defaults.InitialDerbyXML
        self.uuid = uuid.uuid4()
        self.lastSequence = 0
        # self.lastUpdate = 0
        # at some point this stopwatch will be the synchronized bout time
        import Stopwatch
        self.time = Stopwatch.Stopwatch()
        self.time.StartTimer(time.time())
        
        self.boutState = BoutStates.BoutState(self.derbyXML)
        
        self.name = Defaults.NSOName
        self.mdns = None

        self.polling = False
        # not a generally perfect solution
        import socket
        self.address = socket.gethostbyname(socket.gethostname())
        self.foundmDNS = False
        nsoLogger.info("====Created NSOde %s:%d %s %s", self.address,self.port,self.uuid,self.name)
        
        self.poller = zmq.Poller()
#        self.poller.register(self._pipe, zmq.POLLIN)
        self.poller.register(self.inbox, zmq.POLLIN)
#        self.poller.register(self.beacon.get_socket(), zmq.POLLIN)

        self.broadcastIdentity()
        self.lookForPeers()
        while not self.foundmDNS:
            pass

    # mDNS support
    def broadcastIdentity(self):
        """
        Send our identity (our visible name, port, uuid, bout name string) to the world
        @return:
        """
        self.mdns = NSOdeResolver()
        self.mdns.broadcastIdentity(self.name, str(self.port), str(self.uuid), self.bout)
            
    def lookForPeers(self):
        """
                Start looking for peers on the network via an mdns listener
        @return: None
        """

        def addPeer(fullname, hosttarget, port, record):
            """
            Callback from the mdns listener used to add a new peer, or update an existing peer
            that may have had their visible name renamed

            @param fullname: The full name of the peer found
            @param hosttarget: the address of the peer found
            @param port: the remote port number (for connecting to)
            @param record: The encoded TXT record
            @return:
            """
            peerLogger.info("Found peer %s %s %s %s",fullname, hosttarget,port,record)
            uuid = None
            if 'uuid' in record:
                uuid = record['uuid']
            if uuid == str(self.uuid):
                peerLogger.info( "(Thats our UUID!)")
                self.address = ConvertToIPAddress(hosttarget) # this may be too late
                self.foundmDNS = True
                return
            if 'bout' in record and record['bout'] != self.bout:
                peerLogger.info("Not our bout")
            if fullname.split('.')[0] == self.name:
                peerLogger.info("(But that's our name!)")
                self.address = ConvertToIPAddress(hosttarget) # this may be too late
                self.foundmDNS = True
                return
            for peer in self.peers:
                if str(peer.uuid) == uuid:
                    peerLogger.info("(Peer already known)")
                    peer.serviceName = fullname.split('.')[0]
                    return
            peer = self.addPeer(hosttarget,int(port))
            if peer:
                peer.serviceName = fullname.split('.')[0]
        def removePeer(serviceName):
            """
            Handle a peer vanishing from the network

            @param serviceName: The service name of the peer
            """
            for peer in self.peers:
                if peer.serviceName == serviceName:
                    self.removePeer(peer,'mDNS vanished')
                    break
        self.mdns.lookForPeers(addPeer, removePeer)
        
        
    
    def addPeer(self, address, port, uuid = None, introducedBy = None):
        """ Add a peer (given by address/port pair) to the list of peers, if not already there """
        address = ConvertToIPAddress(address)
        for peer in self.peers:
            if (peer.address == address and peer.port == port) or (uuid and uuid == peer.uuid):
                peerLogger.debug("Skipping adding peer %s:%d, already there", address, port)
                return peer # already found as a peer
        peerLogger.debug("Adding peer %s:%d %s", address, port, uuid)
        peer = NSOdePeer(self.ctx, address, port, self, uuid = uuid)
        self.peers.append(peer)
        peer.connect()
        if uuid: # and if we know the uuid, set it after we connect
            peer.uuid = uuid
        peerLogger.debug("Created peer %s",peer)
        # be polite and introduce our new peer to every other peer
        # (and those peers will then introduce themselves if they
        # haven't already)
        for other in self.peers:
            if other != peer and other != introducedBy:
                other.tellPeer(PEER,'%s:%d' % (address, port))
        self.didChangeValue("peers",self.peers)
        return peer

    def removePeer(self, peer, reason=''):
        if peer in self.peers:
            peerLogger.debug("Removing Peer %s",peer)
            peer.tellPeer(GBYE,reason)
            peer.disconnect()
            self.peers.remove(peer)
            self.didChangeValue("peers",self.peers)

    # outgoing messages          
    def chat(self, msg):
        """ Send a text message to all peers - mostly for testing """
        self.tellAllPeers(CHAT,msg)
    def sendXML(self, xml, peer = None):
        if xml is not None:
            if not xml.get('sequence'):
                self.lastSequence += 1
                xml.set('sequence', str(self.lastSequence))
            if not xml.get('source'):
                xml.set('source', self.name)
            self.gossip.recordMessage(xml)
            body = ET.tostring(xml)
            if body:
                if peer:
                    peer.tellPeer(XMSG, body)
                else:
                    self.tellAllPeers(XMSG,body)
        
    def tellAllPeers(self, msg, body):
        """ Send a message (as a UTF8 unicode string) to all peers """
        for peer in self.peers:
            peer.tellPeer(msg,body)

    # handling of incoming messages
    def handleHelloMessage(self,id,address,port):
        # an HELO message may not have an existing peer - if there isn't
        # one, add as a peer.  If there is one, make sure its id matches
        foundPeer = False
        peer = None
        for peer in self.peers:
            if peer.address == address and peer.port == port:
                foundPeer = True
                if id == self.uuid:
                    msgLogger.debug("Talking to myself!")
                    self.removePeer(peer)
                    break
                peer.uuid = id
                peerLogger.info("Met %s", peer)
                break
        if not foundPeer:
            peer = self.addPeer(address,port, id)
        if self.name:
            peer.tellPeer(NAME,self.name)
            # if not foundPeer:
            #     # send our initial state to them
            #     initial = ET.Element("set",{"path": "/"});
            #     initial.append(self.derbyXML)
            #     self.sendXML(initial)
        # self.AskForUpdates(peer)
        
    def handlePeerMessage(self, peer, body):
        # the peer is the person who sent us the introduction, not the
        # node being introduced (which is in the body)
        address,port = body.split(":")
        # don't bother to tell the peer who introduced us about the new peer
        self.addPeer(address,int(port), introducedBy = peer)

    def handleChatMessage(self, peer, body):
        msgLogger.info("%s: says %s", peer,body)
        
    def handleNameMessage(self, peer, body):
        msgLogger.info("%s new name %s", peer,body)
        peer.serviceName = body
    
    def handleGoodbyeMessage(self, peer):
        msgLogger.info("goodbye %s", peer)
        peer.disconnect()
        self.peers.remove(peer)

    def handleGossipMessage(self, peer, body):
        msgLogger.info("%s gossip %s", peer, body)
        source, sequence = body.split("/")
        sequence = int(sequence)
        if source == self.name:
            if self.lastSequence > sequence:
                # they say they've heard N from us, but we are actually at M (>N)
                # so play them back
                for num in range(sequence+1, self.lastSequence+1):
                    peer.tellPeer(XMSG, self.gossip.playback(self.name, num))
        elif self.gossip.gossip(source, sequence):
            # we have a problem
            xmlGSipLogger.warning("xml missing according to gossip: %d vs %s", self.gossip.latestFromPeer(source), body)
            self.catchUpGossip(source)

    def catchUpGossip(self, source):
        """
        Send out messages to try to catch up on gossip
        @param source: who we are behind on (the serviceName)
        """
        lastKnown = self.gossip.latestFromPeer(source)
        msg = "%s/%d" %(source,lastKnown)
        for peer in self.peers:
            if peer.serviceName == source:
                peer.tellPeer(GSIP, msg) # we found the peer that we are behind
                return
        self.tellAllPeers(GSIP, msg)

    def handleXMLMessage(self, peer, body):
        root = None
        try:
                root = ET.fromstring(body)
        except:
                pass
        if root is not None:
                self.handleXML(root, peer)

    def handleXML(self, xml, peer):
        # deal with gossip first
        gossipResult = self.gossip.recordMessage(xml)
        if gossipResult is not True:
            xmlGSipLogger.warning("xml missing: %d vs %s", self.gossip.latestFromPeer(xml.get('source')), ET.tostring(xml))
            self.catchUpGossip(peer.serviceName)
            return # we have a problem
        if xml.tag == "set":
            value = xml.text
            if value: # set text value
                self.SetXMLInTree(value, **xml.attrib)
            elif len(xml.getchildren()) > 0:
                self.SetXMLInTree(xml[0], **xml.attrib)
            else:
                self.SetXMLInTree(None, **xml.attrib)
        elif xml.tag == "add":
            self.AddXMLInTree(xml[0], **xml.attrib)
        elif xml.tag == "replace":
            pass
            # self.lastUpdate = self.time.elapsedTime
#            self.ReplaceXMLInTree(xml[0], **xml.attrib)
        elif xml.tag == "remove":
            pass
            # self.lastUpdate = self.time.elapsedTime
        elif xml.tag == "update":
            print "XML Update request received", ET.tostring(xml)
            updateTime = float(xml.text)
            # if updateTime < self.lastUpdate:
            #     print "We need to send an update to ", peer
            #     #msgs = self.newerXMLSince(self.derbyXML, updateTime)
            #     msg = ET.Element("replace", {"path":"./"})
            #     msg.append(self.derbyXML._xml) #ugh!
            #     self.sendXML(msg, peer)
            #     #for msg in msgs:
            #     #        print "Updating", ET.tostring(msg)
            #     #        self.sendXML(msg, peer)
                
        xmlLogger.debug("xml message: %s",ET.tostring(xml))
        
    def newerXMLSince(self, xml, updateTime, path = ""):
        retval = []
        seen = []
        for child in self.derbyXML.children:
                nextPath = path + "/" + child.elementName + "[" + str(seen.count(child.elementName)) + "]"
                if child.modDate > updateTime:
                        # sent the attributes
                        msg = ET.Element("replace",{"path":nextPath})
                        msg.append(child._xml) # ugh
                        retval.append(msg)
                seen.append(child.elementName)
                if child.latestModDate > updateTime:
                        # send some child if needed
                        retval = retval + self.newerXMLSince(child, updateTime, nextPath)
        return retval

    def handleMessage(self, peer, msg, body):
        msgRecvLogger.debug("%s => %s:%s",peer, msg, body)
        if msg == PEER:
            self.handlePeerMessage(peer,body)
        elif msg == CHAT:
            self.handleChatMessage(peer,body)
        elif msg == NAME:
            self.handleNameMessage(peer,body)
        elif msg == GBYE:
            self.handleGoodbyeMessage(peer)
        elif msg == XMSG:
            self.handleXMLMessage(peer, body)
        elif msg == GSIP:
            self.handleGossipMessage(peer, body)

    def receiveMessage(self):
        frames = self.inbox.recv_multipart()
        #if self.verbose: print "Received",frames
        # Router socket tells us the identity of this peer
        id = None
        if self.inbox.socket_type == zmq.ROUTER:
            address = frames.pop(0)
            id = uuid.UUID(bytes=address)
            #if self.verbose: print("id from router sock: %s" %id)

        msg = frames.pop(0)
        if frames:
                body = frames.pop(0)
        else:
                # for unknown reasons, we sometimes get the message and body in two messages
                msgLogger.warning("Missing body from frame %s %s",msg, self.inbox)
                return
                frames = self.inbox.recv_multipart()
                body = frames.pop(0)
        if msg == HELO:
            address,port = body.split(":")
            self.handleHelloMessage(id,address,int(port))
        else:
            # find the peer that send it
            for peer in self.peers:
                if peer.uuid == id:
                    self.handleMessage(peer,msg,body)
                    return
            msgLogger.warning("Message from unknown peer %s",id)

    def poll(self, timeout = 0):
        items = dict(self.poller.poll(timeout*1000))
        #if self.verbose and items: print "Poll:",items
        if self.inbox in items and items[self.inbox] == zmq.POLLIN:
            self.receiveMessage()
    def pollInBackground(self):
        self.polling = True
        def background():
            while self.polling:
                self.poll(3)
            nsoLogger.info("<End Polling>")
        thread = threading.Thread(target=background)
        #thread.daemon = True
        thread.start()
    def stopPolling(self):
        self.polling = False
            

    # xml tree merging support
    def FindInTree(self,**specifier):
        path = specifier.get('path')
        if path:
            retval = self.derbyXML.find(path)
            if retval is None:
                xmlLogger.warning( "Couldn't find path %s in %s", path, self.derbyXML.tostring())
            else:
                return retval
        # deal with <xxx period='1'> and <xxx period='1' jam='5'>
        periodNum = specifier.get('period')
        if periodNum:
                period = self.derbyXML.periods()[int(periodNum)-1]
                jamNum = specifier.get('jam')
                if jamNum:
                        return period.jams()[int(jamNum)-1]
                else:
                        return period
        eid = specifier.get('eid')
        if eid:
                event = self.derbyXML.find(".//*[@eid='%s']" % (eid,))
                if event is not None:
                        return event
        xmlLogger.warning("Can not find in tree %s",specifier)
        return None
    def UpdateBoutStateForXML(self, xml):
        self.boutState.UpdateBoutState(xml)

    def SetXMLInTree(self,value,**specifier):
        dxml = self.FindInTree(**specifier)
        if dxml is not None:
            attr = specifier.get('attribute')
            if type(value) == type(0) or type(value) == type(0.0):
                value = str(value)
            if value is None:
                if attr:
                    #print "SetXML",xml,attr,None
                    dxml[attr] = None
                else:
                    #print "SetXML.text",xml,None
                    dxml.text = None
            elif type(value) == type(''):
                if attr:
                    #print "SetXML",xml,attr,value
                    dxml[attr] = value
                else:
                    #print "SetXML.text",xml,value
                    dxml.text = value
            else:
                xmlLogger.warning("Didn't SetXML %s %s %s",dxml,attr,value)
                pass
            # notify any observers
            if specifier.get('path'):
                if attr:
                    self.didChangeValue("DerbyXML:" + specifier['path'] + "@" + attr,value)
                else:
                    self.didChangeValue("DerbyXML:" + specifier['path'],value)
            self.UpdateBoutStateForXML(dxml)
            self.NotifyWatchers(dxml)
            # self.lastUpdate = dxml.modDate = self.time.elapsedTime
            xmlSetLogger.debug("Set xml %s",dxml.tostring())
        else:
            xmlLogger.warning("SetXMLInTree failed to find node %s",specifier)

    def AddXMLInTree(self,xml,**specifier):
        xmlAddLogger.debug( "Adding xml %s at %s",ET.tostring(xml),specifier)
        parent = self.FindInTree(**specifier)
        if parent is not None:
                child = parent.addChildXML(xml)
                self.UpdateBoutStateForXML(child)
                self.NotifyWatchers(child)
                # self.lastUpdate = child.lastUpdate = self.time.elapsedTime

        if nsoXMLLogger.isEnabledFor(logging.DEBUG):
                nsoXMLLogger.debug(self.derbyXML.tostring())
                
    def RemoveXMLInTree(self,**specifier):
        xmlDelLogger.error("Remove XML In Tree not supported yet")

    def ReplaceXMLInTree(self,xml, **specifier):
        xmlReplLogger.debug("Replacing xml %s at %s", ET.tostring(xml), specifier)
        target = self.FindInTree(**specifier)
        if target is not None:
                target.takeAttributesAndChildrenFrom(xml)
                self.UpdateBoutStateForXML(target)
                self.NotifyWatchers(target)
                # self.lastUpdate = target.modDate = self.time.elapsedTime
        if nsoXMLLogger.isEnabledFor(logging.DEBUG):
                nsoXMLLogger.debug(self.derbyXML.tostring())

    # higher level event support - send the events to our peers, and then call the tree manipulation routines (as if we had heard about this)
    def SetXML(self, xml, **kwds):
        """ Send xml that is at a specific node """
        set = ET.Element("set",kwds)
        if xml is None:
            pass
        elif type(xml) == type(''):
            set.text = xml
        else:
            set.append(xml)
        self.sendXML(set)
        self.SetXMLInTree(xml, **kwds)

    def AddXML(self, xml, **kwds):
        """ Add an element to a node specified by a specifier """
        add = ET.Element("add",kwds)
        xml.set('eid',str(uuid.uuid4())) # set the eid for an element
        add.append(xml)
        self.sendXML(add)
        self.AddXMLInTree(xml, **kwds)
        
    def RemoveXML(self, **kwds):
        """ Remove an element to a node """
        add = ET.Element("remove",kwds)
        add.append(xml)
        self.sendXML(add)
        self.RemoveXMLInTree(kwds)

    def ReplaceXML(self, xml, **kwds):
        """ Replace an existing element with another """
        repl = ET.Element('replace', kwds)
        repl.append(xml)
        self.sendXML(repl)
        self.ReplaceXMLInTree(xml, kwds)
    
    # def AskForUpdates(self, peer = None):
    #     """ Ask for any updates, either from everybody or else from a specific peer """
    #     ask = ET.Element('update')
    #     ask.text = str(self.lastUpdate)
    #     self.sendXML(ask)
        
    # Get the "current" (live) jam,period,etc
    def CurrentPeriod(self):
        retval = None
        for period in self.derbyXML.periods():
            if period.timestamp is None:
                continue # this can't be the current one, no timestamp yet
            retval = period
        if retval is None: # not started, use first period
            retval = self.derbyXML.periods()[0]
        return retval
    def CurrentJam(self):
        period = self.CurrentPeriod()
        retval = None
        for child in period.children:
                if child.elementName == "jam":
                        if child.timestamp:
                                retval = child
                        elif retval is None:
                                retval = child # better than nothing
        return retval
        
    def CurrentTimeout(self):
        period = self.CurrentPeriod()
        retval = None
        for child in period.children:
                if child.elementName == "time-out" or child.elementName == "official-review":
                        if child.timestamp:
                                retval = child
                        elif retval is None:
                                retval = child # better than nothing
        return retval
    # Actual high level events - the UI should call these methods to deal with the bout state
    def StartJam(self, jam = None, period = None):
        if period is None:
                periodXML = self.CurrentPeriod()
                period = self.derbyXML.periods().index(periodXML)+1
        else:
                periodXML = self.derbyXML.periods()[int(period)-1]
        now = SecondsToHMS(time.time())
        if periodXML.timestamp is None: # start the period at the current clock time
                self.SetXML(now,period=str(period),attribute='timestamp')
        jams = periodXML.jams()
        if 0:
                ts = Timestamp(now, periodXML)
                tspmmss = ts.PMMSSStr() # get the timestamp relative to the period clock
        tspmmss = now # for now, use absolute time stamps
        if jam is None or jam > len(jams):
                jam = len(jams)+1 # start a new jam
                xml = ET.Element("jam",{'number':str(jam),'timestamp':tspmmss})
                nsoLogger.debug( "Starting new jam %s %s at %s:%s in period %s",period,jam,tspmmss,xml,periodXML)
                self.AddXML(xml, period=str(period))
        else:
                xml = jams[jam-1]
                if xml.timestamp is None:
                        ts = Timestamp(str(time.time()))
                        nsoLogger.debug( "Starting existing jam %s %s at %s",period,jam,tspmmss)
                        self.SetXML(tspmmss,path="jam[@eid='%s']" % xml['eid'], attribute="timestamp")
                else:
                        pass # jam is already running
                
    def StopJam(self, jam = None, period = None):
        dxml = self.CurrentJam()
        nsoLogger.debug("Stopping current jam %s",dxml.tostring())
        if dxml and dxml['duration'] is None and dxml.timestamp:
                start = dxml.timestamp.Absolute()
                duration = round(time.time() - start)
                self.SetXML(str(duration),eid=dxml['eid'],attribute='duration')
    def StartTimeout(self, team = None, period = None):
        if period is None:
                periodXML = self.CurrentPeriod()
                period = self.derbyXML.periods().index(periodXML)+1
        else:
                periodXML = self.derbyXML.periods()[int(period)-1]
        tspmmss = SecondsToHMS(time.time())
        xml = ET.Element("time-out",{'timestamp':tspmmss})
        nsoLogger.debug("Starting timeout %s at %s:%s in period %s",period,tspmmss,xml,periodXML)
        self.AddXML(xml, period=str(period))
    def StopTimeout(self, timeout = None, period = None):
        dxml = self.CurrentTimeout()
        if dxml and dxml['duration'] is None and dxml.timestamp:
                start = dxml.timestamp.Absolute()
                duration = round(time.time() - start)
                self.SetXML(str(duration),eid=dxml['eid'],attribute='duration')
    def Lineup(self,team,skater,position, jam = None, period = None):
        xml = ET.Element("line-up",{"team":team, "skater":skater, "position":position})
        self.AddXML(xml, jam=jam,period=period)
    def ScorePass(self,team,points,skater=None, jam = None, period = None):
        if jam is None:
                jamDXML = self.CurrentJam()
                periodDXML = jamDXML.EnclosingPeriod()
                jam = periodDXML.index(jamDXML)+1
        if period is None:
                periodDXML = self.CurrentPeriod()
                period = self.derbyXML.periods().index(periodDXML)+1
        xml = ET.Element("pass",{"team":team,"points":points})
        self.AddXML(xml, team=team, jam=str(jam), period=str(period))
    def UpdateTimer(self, timer, value):
        if Defaults.SendTimers:
                self.SetXML(str(value),path="timer[@id='%s']" % timer)
    def StartTimer(self, timer):
        if Defaults.SendTimers:
                self.SetXML("yes",path="timer[@id='%s']" % timer,attribute='running')
    def StopTimer(self, timer):
        if Defaults.SendTimers:
                self.SetXML(None,path="timer[@id='%s']" % timer,attribute='running')

    # Watcher client
    def AddWatcher(self, tagname, watcher):
        watchersForTag = self.watchers.get(tagname)
        if watchersForTag is None:
            watchersForTag = []
            self.watchers[tagname] = watchersForTag
        watchersForTag.append(watcher)
    def RemoveWatcher(self, watcher):
        for tag in self.watchers:
            watchersForTag = self.watchers[tag]
            while watcher in watchersForTag:
                watchersForTag.delete(watcher)
    def NotifyWatchers(self, xml):
        tagname = xml.elementName
        allNodes = self.derbyXML.allNodesWithTag(tagname)
        for watcher in self.watchers.get(tagname,[]):
            watcher(allNodes)
        for watcher in self.watchers.get("*",[]):
            watcher([xml])
            
    # debugging
    def dumpPeers(self):
        for peer in self.peers:
            print peer
    def dumpXML(self):
        print self.derbyXML.tostring()
if __name__ == '__main__':
    ctx = zmq.Context()
    n = NSOde(ctx)
    n.pollInBackground()
    while True:
        try:
            print "NSOde>",
            msg = input()
            print msg
        except (KeyboardInterrupt, SystemExit):
            break
        except:
            n.stopPolling()
    print("FINISHED")
    